部署 Racket Web 应用
最近有人在 Racket Slack 上询问如何部署 Racket Web 应用。最常见的答案有
- 在目标机器上安装 Racket,然后将你的代码传送到那里或
- 使用 Docker(基本上是选项 1 的一个“可移植”变体)。
我想花几分钟时间今天来写一下我部署 Racket 应用的首选方式:将应用程序代码、库和资源嵌入到一个可执行文件中,然后分发这个文件。我偏爱这种做法,因为它意味着我不必担心在目标机器上安装特定版本的 Racket 来运行我的代码。事实上,使用这种方法,我可以为每个应用拥有不同版本,每个版本都使用不同版本的 Racket 构建,并且可以轻松地在它们之间切换。
raco exe 将 Racket 模块以及运行时嵌入到平台的原生可执行文件中。以这个程序为例:
#lang racket/base
(require racket/async-channel
web-server/http
web-server/servlet-dispatch
web-server/web-server)
(define ch (make-async-channel))
(define stop
(serve
#:dispatch (dispatch/servlet
(lambda (_req)
(response/xexpr
'(h1 "Hello!"))))
#:port 8000
#:listen-ip "127.0.0.1"
#:confirmation-channel ch))
(define ready-or-exn (sync ch))
(when (exn:fail? ready-or-exn)
(raise ready-or-exn))
(with-handlers ([exn:break?
(lambda (_)
(stop))])
(sync/enable-break never-evt))
如果我将它保存为名为 app.rkt 的文件,然后调用 raco exe -o app app.rkt ,我会在当前目录中得到一个自包含的可执行文件,名为 app 。
$ file app
app: Mach-O 64-bit executable x86_64
生成的可执行文件可能仍然引用仅在当前机器上可用的动态库,所以在这个阶段它还不太适合分发。这时 raco distribute 就派上用场了。它接受 raco exe 创建的独立可执行文件,并生成一个包含可执行文件、它引用的动态库以及应用引用的任何运行时文件(稍后会有更多关于这方面的内容)的包。然后可以将这个包复制到运行相同操作系统的其他机器上。
运行 raco distribute dist app 会生成一个包含以下内容的目录:
$ raco distribute dist app
$ tree dist/
dist/
├── bin
│ └── app
└── lib
├── Racket.framework
│ └── Versions
│ └── 7.7.0.9_CS
│ ├── Racket
│ └── boot
│ ├── petite.boot
│ ├── racket.boot
│ └── scheme.boot
└── plt
└── app
└── exts
└── ert
├── r0
│ └── error.css
├── r1
│ ├── libcrypto.1.1.dylib
│ └── libssl.1.1.dylib
└── r2
└── bundles
├── es
│ └── srfi-19
└── srfi-19
15 directories, 10 files
我可以将这个目录压缩打包,然后发送到任何运行与我相同版本 macOS 的机器上,它无需修改即可运行。如果在 Linux 机器上构建代码,然后将其发送到其他 Linux 机器上运行,情况也是如此。这就是我分发我的 Web 应用时所做的事情。每个项目都有一个 CI 任务,用于构建和测试代码,然后生成分发版本,并将其复制到目标服务器。
此时你可能会想“这很好,但运行时应用程序需要哪些文件呢?”让我们修改应用程序,使其从磁盘读取文件,然后按需提供其内容:
#lang racket/base
(require racket/async-channel
racket/port
web-server/http
web-server/servlet-dispatch
web-server/web-server)
(define text
(call-with-input-file "example.txt" port->string))
(define ch (make-async-channel))
(define stop
(serve
#:dispatch (dispatch/servlet
(lambda (_req)
(response/xexpr
`(h1 ,text))))
#:port 8000
#:listen-ip "127.0.0.1"
#:confirmation-channel ch))
(define ready-or-exn (sync ch))
(when (exn:fail? ready-or-exn)
(raise ready-or-exn))
(with-handlers ([exn:break?
(lambda (_)
(stop))])
(sync/enable-break never-evt))
如果我只是拿这个应用,构建一个可执行文件,然后制作一个发行版,再尝试运行它,我会遇到一个问题:
$ raco exe -o app app.rkt
$ raco distribute dist app
$ cd dist
$ ./bin/app
open-input-file: cannot open input file
path: /Users/bogdan/tmp/dist/example.txt
system error: No such file or directory; errno=2
context...:
raise-filesystem-error
open-input-file
call-with-input-file
proc
call-in-empty-metacontinuation-frame
call-with-module-prompt
body of '#%mzc:s
temp35_0
run-module-instance!
perform-require!
call-in-empty-metacontinuation-frame
eval-one-top
eval-compiled-parts
embedded-load
proc
call-in-empty-metacontinuation-frame
如果我没有 cd 进入到 dist 目录,这本来是可以工作的,因为 example.txt 将会在应用程序运行的工作目录中。问题在于我们传递给 call-with-input-file 的路径在编译时 Racket 一无所知。
为了将 example.txt 文件与应用程序一起发布,我们需要使用 define-runtime-path 来告诉 Racket 它应该在分发中嵌入该文件,并更新代码以便它引用嵌入文件最终的路径。
#lang racket/base
(require racket/async-channel
racket/port
+ racket/runtime-path
web-server/http
web-server/servlet-dispatch
web-server/web-server)
+
+(define-runtime-path example-path "example.txt")
(define text
- (call-with-input-file "example.txt" port->string))
+ (call-with-input-file example-path port->string))
(define ch (make-async-channel))
(define stop
(serve
#:dispatch (dispatch/servlet
(lambda (_req)
(response/xexpr
`(h1 ,text))))
#:port 8000
#:listen-ip "127.0.0.1"
#:confirmation-channel ch))
(define ready-or-exn (sync ch))
(when (exn:fail? ready-or-exn)
(raise ready-or-exn))
(with-handlers ([exn:break?
(lambda (_)
(stop))])
(sync/enable-break never-evt))
上述代码中使用 define-runtime-path 告诉 raco distribute 将 example.txt 复制到分发目录中,并确保 example-path 绑定指向该文件最终所在的路径。
如果我现在构建一个发行版并检查其内容,我可以看到 example.txt 被复制进去了:
$ raco exe -o app app.rkt
$ raco distribute dist app
$ tree dist
dist/
├── bin
│ └── app
└── lib
├── Racket.framework
│ └── Versions
│ └── 7.7.0.9_CS
│ ├── Racket
│ └── boot
│ ├── petite.boot
│ ├── racket.boot
│ └── scheme.boot
└── plt
└── app
└── exts
└── ert
├── r0
│ └── example.txt
├── r1
│ └── error.css
├── r2
│ ├── libcrypto.1.1.dylib
│ └── libssl.1.1.dylib
└── r3
└── bundles
├── es
│ └── srfi-19
└── srfi-19
16 directories, 11 files
如果你想知道更多关于这一切是如何运作的,我提供的关于 raco exe、raco distribute 和 define-runtime-path 的链接应该能帮到你!